Skip to content

Chat: tell a banned user what happened and when it lifts - #3479

Merged
feruzm merged 3 commits into
developmentfrom
bugfix/chat-ban-messaging
Aug 11, 2026
Merged

Chat: tell a banned user what happened and when it lifts#3479
feruzm merged 3 commits into
developmentfrom
bugfix/chat-ban-messaging

Conversation

@feruzm

@feruzm feruzm commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #3470. Mobile counterpart to ecency/vision-web#1390 (merged).

Before

sendMattermostMessage detected the ban but built an error carrying only the message, dropping bannedUntil and reason. The UI showed a one-shot toast:

Unusual activity detected. Please try again after some time.

No duration, so a 48-hour timeout and a 3-year ban read identically. And because it was a toast, once dismissed every later send failed with no standing explanation.

After

A persistent banner above the composer:

You are paused from posting because the same message went to several channels at once.
You can still read chat. Posting unlocks in about 2 days.

Changes

  • providers/chat/mattermost.ts preserves bannedUntil and reason on the thrown error
  • screens/chats/utils/chatBanNotice.ts builds the copy, mirroring the web formatter with the same reasons and the same duration bands, so the two clients cannot describe one moderation action differently. formatMessage is injected rather than imported, keeping it a pure testable unit
  • screens/chats/children/ChatBanBanner.tsx renders it above the composer, next to the action that is blocked, following the existing DmWarningBanner pattern
  • nine chats.ban-* keys added to the Crowdin source

Details worth knowing

The banner ticks. It re-renders on a bounded interval instead of freezing at first render, and clears both at expiry and on a successful send, which is how an early moderator unban gets picked up without a reload.

The tick is deliberately a fixed interval, not a delay derived from bannedUntil. setTimeout takes a 32-bit signed delay, so a 3-year ban resolves to ~1ms and would clear its own notice instantly, hiding it from exactly the users who are most banned. That bit the web PR; there is a test for it here.

Duration bands never produce a count of 1, so nothing reads "in about 1 hours" and these stay plain keys rather than needing plural variants.

The old toast remains as a fallback for a ban with no usable expiry, so an older server still says something rather than nothing.

Unknown reasons degrade to generic copy: live bans predate the reason prop, and a newer moderation service may add reasons this build has not seen.

Checks

Full suite green (756 passed, 51 suites), 14 new tests. typecheck ok: 0 errors (baseline 0). Lint on the touched files is unchanged from development (12 warnings, 0 errors, all pre-existing).

The locale addition is a byte-preserving textual insert. A json.dump round-trip rewrote existing \uXXXX escapes across the file, so the diff here is 10 added lines rather than a whole-file reformat that would have churned the Crowdin source.

Summary by CodeRabbit

  • New Features

    • Added a persistent chat restriction banner showing the reason and remaining duration.
    • Ban notices update automatically and disappear when the restriction expires.
    • Added localized messages for emoji search results and temporary posting restrictions.
  • Bug Fixes

    • Improved handling of chat restriction details, including expiry times and moderation reasons.
    • Retained a fallback notification when restriction information is unavailable.
  • Tests

    • Added coverage for restriction validation, formatting, expiration, and privacy-safe messaging.

The provider detected the ban but kept only the message, discarding bannedUntil
and reason, so the UI could not tell a 48-hour timeout from a 3-year ban. It
showed a one-shot toast: 'Unusual activity detected. Please try again after some
time.' A ban is a state, not an event, so once that toast was dismissed every
later send failed with no standing explanation.

Preserves the payload and replaces the toast with a persistent banner above the
composer, carrying the same copy and duration bands as the web client so the two
clients cannot describe one moderation action differently.

The banner ticks rather than freezing at first render, and clears itself both at
expiry and on a successful send, which is how an early moderator unban is picked
up. The tick is a bounded interval: a delay derived from bannedUntil overflows
the 32-bit setTimeout limit and would fire immediately for a multi-year ban.

The old toast stays as the fallback for a ban with no usable expiry, so an older
server still says something rather than nothing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c70cc2345f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// A send that lands proves the ban is gone, which is how an early moderator unban
// clears the banner instead of it lingering until the original expiry.
setBanInfo(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the ban only after a new post succeeds

When a banned user successfully edits an existing message, _handleSend takes the updateMattermostMessage PATCH branch and then clears banInfo here. A successful edit does not prove that the separate create-post restriction has been lifted, so the standing notice disappears even though subsequent new messages may still be rejected; move this clear into the sendMattermostMessage branch.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a1f10ce-eb01-4ae5-a735-e19a5f6d6466

📥 Commits

Reviewing files that changed from the base of the PR and between c70cc23 and 41847bd.

📒 Files selected for processing (4)
  • src/screens/chats/children/ChatBanBanner.tsx
  • src/screens/chats/container/chatThreadContainer.tsx
  • src/screens/chats/utils/chatBanNotice.test.ts
  • src/screens/chats/utils/chatBanNotice.ts
📝 Walkthrough

Walkthrough

The chat flow now preserves structured ban metadata, formats localized duration-aware notices, and displays a persistent banner until the ban expires. The container clears the banner after successful sends or expiration.

Changes

Chat ban notices

Layer / File(s) Summary
Ban notice utilities and localization
src/screens/chats/utils/chatBanNotice.ts, src/config/locales/en-US.json, src/screens/chats/utils/chatBanNotice.test.ts
Added ban validation, duration formatting, reason-specific notices, localized messages, and utility tests.
Structured ban error handling
src/providers/chat/mattermost.ts, src/screens/chats/container/chatThreadContainer.tsx
Mattermost ban errors now preserve bannedUntil and reason. The chat container stores structured ban data and clears it after successful sends.
Persistent banner lifecycle
src/screens/chats/children/ChatBanBanner.tsx, src/screens/chats/container/chatThreadContainer.tsx
Added a localized banner that refreshes every 30 seconds, stops at expiry, and clears container state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Mattermost
  participant ChatThreadContainer
  participant chatBanNotice
  participant ChatBanBanner
  Mattermost-->>ChatThreadContainer: ban error with bannedUntil and reason
  ChatThreadContainer->>chatBanNotice: getChatBanInfo(error)
  chatBanNotice-->>ChatThreadContainer: ChatBanInfo
  ChatThreadContainer->>ChatBanBanner: render(info)
  ChatBanBanner->>chatBanNotice: formatChatBanNotice(info, now)
  chatBanNotice-->>ChatBanBanner: localized notice
  ChatBanBanner-->>ChatThreadContainer: onExpire()
Loading

Possibly related issues

  • ecency/vision-mobile#3470 — The changes implement structured ban handling and persistent duration-aware notices in the mobile chat thread.
  • ecency/vision-web#1389 — The changes implement the mobile portion of persistent localized ban notices with reasons and expiry data.

Poem

A rabbit sees the ban clock glow,
With minutes left, then hours slow.
The banner hops through time with care,
While localized words fill the air.
At expiry, it fades from view—
And sending starts anew.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: persistent feedback for banned chat users about the ban and its end time.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/chat-ban-messaging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/screens/chats/children/ChatBanBanner.tsx`:
- Around line 25-26: Move the onExpireRef.current assignment out of render and
into a useEffect associated with the ChatBanBanner component, updating it only
after the render commits. Preserve the existing ref usage while preventing
discarded concurrent renders from changing the callback used by the committed
interval.
- Line 46: Update the formatChatBanNotice call in ChatBanBanner to pass
intl.formatMessage directly, removing the any cast and preserving the existing
formatter behavior.

In `@src/screens/chats/container/chatThreadContainer.tsx`:
- Around line 1326-1328: Update onNewMessage’s pendingMatch || fallbackMatch
branch to clear banInfo after WebSocket send confirmation, ensuring the cleanup
runs even when the subsequent HTTP request fails. Preserve the existing banner
behavior for messages without a confirmed pending or fallback match.

In `@src/screens/chats/utils/chatBanNotice.ts`:
- Around line 25-35: Update getChatBanInfo validation to reject all non-finite
bannedUntil values by using Number.isFinite(bannedUntil) alongside the existing
presence and expiry checks. Preserve the current null return and valid
future-expiry handling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee21c3ed-ef88-49c3-813b-b69cd8eed3b3

📥 Commits

Reviewing files that changed from the base of the PR and between 457e15e and c70cc23.

📒 Files selected for processing (6)
  • src/config/locales/en-US.json
  • src/providers/chat/mattermost.ts
  • src/screens/chats/children/ChatBanBanner.tsx
  • src/screens/chats/container/chatThreadContainer.tsx
  • src/screens/chats/utils/chatBanNotice.test.ts
  • src/screens/chats/utils/chatBanNotice.ts

Comment thread src/screens/chats/children/ChatBanBanner.tsx Outdated
Comment thread src/screens/chats/children/ChatBanBanner.tsx Outdated
Comment thread src/screens/chats/container/chatThreadContainer.tsx Outdated
Comment thread src/screens/chats/utils/chatBanNotice.ts
feruzm added 2 commits August 11, 2026 11:06
The clear sat after the edit/create branches, so it ran for both. Editing an
existing message is not ban-gated server-side (only the create-post route checks
isUserChatBanned), so a banned user could edit a message, succeed, and dismiss
the notice while still blocked from posting anything new.

Moved into the create branch, keyed on the request the ban actually gates.
Websocket confirmation of our own send is independent proof the create landed,
and it can arrive when the HTTP response never does. Without clearing there the
banner sat until its original expiry despite the ban being lifted. Both match
arms are create-only, so an edit still cannot dismiss it.

getChatBanInfo used Number.isNaN, which Infinity passes, and Infinity is also
greater than now, so both guards let it through: the banner would render an
endless duration and never fire onExpire. Switched to Number.isFinite, with a
test covering Infinity, -Infinity, the string form and a plain non-number.

Moved the onExpire ref assignment into an effect so a discarded render cannot
mutate the ref the committed interval reads, and dropped the unnecessary any
cast on intl.formatMessage (IntlShape's signature already satisfies the
injected formatter type; typecheck confirms).
@feruzm
feruzm merged commit f0d16e2 into development Aug 11, 2026
12 checks passed
@feruzm
feruzm deleted the bugfix/chat-ban-messaging branch August 11, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat: tell a banned user what happened and when it lifts

1 participant